
/* GCSx
** EDITBOX.CPP
**
** Multi-line textbox window
*/

/*****************************************************************************
** Copyright (C) 2003-2006 Janson
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
** 
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111, USA.
*****************************************************************************/

#include "all.h"

const string EditBox::wordChars("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");

void EditBox::setDirtyAndBlink() { start_func
    insertBlink = 1;
    insertBlinkMs = SDL_GetTicks();
    setDirtyRange(insertion.vRow, insertion.vRow);
}

void EditBox::setDirtyRange(int first, int last) { start_func
    setDirty();
    if (vDirtyFirst == -1) {
        vDirtyFirst = first;
        vDirtyLast = last;
    }
    else {
        if (first < vDirtyFirst) vDirtyFirst = first;
        if (last > vDirtyLast) vDirtyLast = last;
    }
}

void EditBox::goLeft(TextPoint& point) { start_func
    --point.column;
    if (point.column < 0) {
        --point.vRow;
        if (point.vRow < 0) {
            // End of all text- cancel all movement
            ++point.vRow;
            ++point.column;
        }
        else point.column = (*findVLine(point.vRow)).length;
    }
}

void EditBox::goRight(TextPoint& point) { start_func
    ++point.column;
    if (point.column > (*findVLine(point.vRow)).length) {
        ++point.vRow;
        if (point.vRow >= vNumLines) {
            // End of all text- cancel all movement
            --point.vRow;
            --point.column;
        }
        else point.column = 0;
    }
}
void EditBox::vLineToDLine(TextPoint& point) { start_func
    list<VLine>::iterator VL = findVLine(point.vRow);
    assert(VL != lineMap.end());
    point.vRow = (*VL).dLine;
    point.column += (*VL).colStart;
}

void EditBox::dLineToVLine(TextPoint& point) { start_func
    list<VLine>::iterator VL = findDLine(point.vRow, &point.vRow);
    while (1) {
        assert(VL != lineMap.end());
        if (point.column - (*VL).colStart <= (*VL).length) break;
        ++point.vRow;
        ++VL;
    }
    point.column -= (*VL).colStart;
    if (point.column < 0) point.column = 0;
    point.pixelX = fontWidth((*(*VL).dLineI).substr((*VL).colStart, point.column), font);
}

void EditBox::vLineToDLineAllPoints() { start_func
    vLineToDLine(insertion);
    if (isSelect) {
        vLineToDLine(selectBegin);
        vLineToDLine(selectEnd);
    }
}

void EditBox::dLineToVLineAllPoints() { start_func
    dLineToVLine(insertion);
    if (isSelect) {
        dLineToVLine(selectBegin);
        dLineToVLine(selectEnd);
    }
}

void EditBox::resize(int newWidth, int newHeight, int newViewWidth, int newViewHeight, int fromParent) { start_func
    Window::resize(newWidth, newHeight, newViewWidth, newViewHeight, fromParent);

    // Always rewrap even if wordwrap off, if wrappedTo = -1
    if ((viewWidth != wrappedTo) && ((wordwrap) || (wrappedTo == -1))) {
        if (wrappedTo == -1) {
            insertion.vRow = 0;
            insertion.column = 0;
            insertion.pixelX = 0;
            isSelect = 0;

            fillLineMap();
            wrappedTo = viewWidth;
        }
        else {
            vLineToDLineAllPoints();
    
            fillLineMap();
            wrappedTo = viewWidth;
        
            dLineToVLineAllPoints();
        }
        
        setDirty();
        vDirtyFirst = 0;
        vDirtyLast = vNumLines - 1;
    
        Window::resize(GUI_EDITBOX_INNERMARGIN * 2 + GUI_EDITBOX_LEFTPAD + GUI_EDITBOX_RIGHTPAD + vLongestLength,
                       GUI_EDITBOX_INNERMARGIN * 2 + vNumLines * lineHeight);
    }
}

int EditBox::addLineWrapped(int dLine, list<string>::iterator& iter, list<VLine>::iterator& insertAt) { start_func
    // Wrap at width of window
    int wrapAt = viewWidth - GUI_EDITBOX_INNERMARGIN * 2 - GUI_EDITBOX_LEFTPAD - GUI_EDITBOX_RIGHTPAD;
    int size = (*iter).size();
    int count = 0;
    int pos = 0;
    int skip;
    
    do {
        skip = 0;
        
        VLine newMap;
        newMap.dLine = dLine;
        newMap.dLineI = iter;
        newMap.colStart = pos;
        // Start with what's left
        newMap.length = size - pos;
        
        // Don't wrap 0 or 1 characters
        if ((wordwrap) && (newMap.length > 1)) {
            // Wrap text
            string str = newMap.getStr();
            // (always max out at MAX_LINELENGTH)
            int wrapMax = max(2, min(breakLineBefore(str, wrapAt), (int)MAX_LINELENGTH));
            // Any cut?
            if (wrapMax < newMap.length) {
                // Move back until character after wrap is space and before wrap is not
                int wrapMin = wrapMax;
                while ((wrapMin) && ((str[wrapMin] != ' ') || (str[wrapMin - 1] == ' '))) {
                    --wrapMin;
                }
                // If we still have at least one character, wrap at word boundary found
                if (wrapMin > 0) wrapMax = wrapMin;
            }
            newMap.length = wrapMax;
        
            // Skip over all spaces
            while (str[wrapMax++] == ' ') ++skip;
        }
        
        // Skip over what we've used
        pos += newMap.length + skip;
        
        lineMap.insert(insertAt, newMap);

        int width = fontWidth(newMap.getStr(), font);
        if (width > vLongestLength) vLongestLength = width;
        
        ++count;
        // (loop back if anything left OR if we skipped spaces)
    } while ((pos < size) || (skip));
    
    return count;
}

void EditBox::fillLineMap() { start_func
    lineMap.clear();
    
    lastSpot = lineMap.end();
    lastSpotLine = -1;

    if (contents->empty())
        contents->push_back(blankString);
    
    dNumLines = 0;
    vNumLines = 0;
    vLongestLength = 0;

    list<string>::iterator end = contents->end();
    list<VLine>::iterator insertAt = lineMap.end();
    for (list<string>::iterator pos = contents->begin(); pos != end; ++pos) {
        vNumLines += addLineWrapped(dNumLines++, pos, insertAt);
    }
}

void EditBox::dumpLineMap() { start_func
    // @TODO: temporary function for debugging
    list<VLine>::iterator start = lineMap.begin();
    list<VLine>::iterator end = lineMap.end();
    int pos = 0;
    for (; start != end; ++start) {
        debugStdout("[%d] d%d c%d l%d | %s", pos++, (*start).dLine, (*start).colStart, (*start).length, (*start).getStr().c_str());
    }
    list<string>::iterator start2 = contents->begin();
    list<string>::iterator end2 = contents->end();
    pos = 0;
    for (; start2 != end2; ++start2) {
        debugStdout("(%d) %s", pos++, (*start2).c_str());
    }
}

void EditBox::eraseLineMap(int dLineFirst, int count, int* getVLineAt, int* getVLineCount) { start_func
    int vlNum;
    list<VLine>::iterator start = findDLine(dLineFirst, &vlNum);
    list<VLine>::iterator end = start;
    
    // Count elements being erased
    int vCount = 0;
    list<VLine>::iterator final = lineMap.end();
    for (; end != final; ++end) {
        if ((*end).dLine == dLineFirst + count) break;
        ++vCount;
        // (carry lastspot along for the ride)
        if ((lastSpotLine >= 0) && (lastSpot == end)) {
            ++lastSpot;
            if (lastSpot == final) lastSpotLine = -1;
            else ++lastSpotLine;
        }
    }
    
    // (final adjust to lastspot- safe if spot is -1)
    if (lastSpotLine >= vlNum) {
        assert(lastSpotLine >= vlNum + vCount);
        lastSpotLine -= vCount;
    }

    lineMap.erase(start, end);
    vNumLines -= vCount;
    dNumLines -= count;
    
    // Decrement dline references
    for (; end != final; ++end) {
        (*end).dLine -= count;
    }
    
    if (getVLineAt) *getVLineAt = vlNum;
    if (getVLineCount) *getVLineCount = vCount;
    
#ifndef NDEBUG
    if (lastSpotLine >= 0) {
        vlNum = lastSpotLine;
        start = lastSpot;
        lastSpot = lineMap.end();
        lastSpotLine = -1;
        end = findVLine(vlNum);
        assert(start == end);
    }
#endif
}

void EditBox::insertLineMap(int dLineAt, int count, int* getVLineAt, int* getVLineCount) { start_func
    // Position in VLines to add at
    int vlNum;
    list<VLine>::iterator insertAt = findDLine(dLineAt, &vlNum);
    if (vlNum == -1) vlNum = vNumLines;
    
    // Position in DLines to add from
    list<string>::iterator insertFrom;
    if (dLineAt == 0)
        insertFrom = contents->begin();
    else {
        list<VLine>::iterator insertAt2 = insertAt;
        --insertAt2;
        insertFrom = (*insertAt2).dLineI;
        ++insertFrom;
    }
    
    int vCount = 0;
    for (int did = 0; did < count; ++did) {
        vCount += addLineWrapped(dLineAt++, insertFrom, insertAt);
        ++insertFrom;
        ++dNumLines;
    }
    
    vNumLines += vCount;
    // (-1 safely ignored here)
    if (lastSpotLine >= vlNum) lastSpotLine += vCount;
    
    // Increment dline references
    list<VLine>::iterator end = lineMap.end();
    for (; insertAt != end; ++insertAt) {
        (*insertAt).dLine += count;
    }
    
    if (getVLineAt) *getVLineAt = vlNum;
    if (getVLineCount) *getVLineCount = vCount;
}

EditBox::EditBox(list<string>* storage, int myFont, int isReadOnly) : Window() { start_func
    readonly = isReadOnly;
    font = myFont;
    contents = storage;
    wordwrap = 1;
    autoIndent = 0;
    wrappedTo = -1;
    
    lastSpot = lineMap.end();
    lastSpotLine = -1;
    
    lineHeight = fontHeight(font);
    insertYOffset = 0;
    insertHeight = lineHeight;

    myFrame = NULL;
    myScroll = NULL;
    open = 0;
    haveFocus = 0;
}

void EditBox::prepOpen() { start_func
    // Simply "resizes", which forces init if not done yet
    resize(width, height);
}

EditBox::~EditBox() { start_func
}
    
int EditBox::event(int hasFocus, const SDL_Event* event) { start_func
    TextPoint point;
    list<VLine>::iterator pointVLine;
    string::size_type foundStr;
    string clipStorage;

    switch (event->type) {
        case SDL_CLOSE:
            open = 0;
            myFrame = NULL;
            myScroll = NULL;
            return 1;
            
        case SDL_MOUSEBUTTONDOWN:
            if (event->button.button == SDL_BUTTON_LEFT) {
                point = insertWhere(event->button.x, event->button.y);
                insertionState(point, SDL_GetModState() & KMOD_SHIFT);
                return 1;
            }
            break;
    
        case SDL_MOUSEBUTTONDBL:
            // Where clicked
            point = insertWhere(event->button.x, event->button.y);
            pointVLine = findVLine(point.vRow);
            
            // Find first word character to highlight
            --point.column;
            if (point.column < 0) point.column = 0;
            foundStr = (*pointVLine).getStr().find_last_not_of(wordChars, point.column);
            if (foundStr == string::npos) point.column = 0;
            else point.column = foundStr + 1;
            insertionState(point, 0);
            
            // Find last character to highlight
            foundStr = (*pointVLine).getStr().find_first_not_of(wordChars, point.column);
            if (foundStr == string::npos) point.column = (*pointVLine).length;
            else point.column = foundStr;
            insertionState(point, 1);
            return 1;
    
        case SDL_MOUSEMOTION:
            if (event->motion.state & SDL_BUTTON_LMASK) {
                point = insertWhere((Sint16)event->motion.x, (Sint16)event->motion.y);
                insertionState(point, 1);
                return 1;
            }
            break;
        
        case SDL_MOUSEFOCUS:
            if (event->user.code & 1) {
                selectMouse(MOUSE_BEAM);
            }
            else {
                selectMouse(MOUSE_NORMAL);
            }
            return 1;

        case SDL_INPUTFOCUS:
            if (event->user.code & 1) {
                if (!haveFocus) {
                    haveFocus = 1;
                    if (isSelect) setDirtyRange(selectBegin.vRow, selectEnd.vRow);
                    setDirtyAndBlink();
                }
            }
            else {
                if (haveFocus) {
                    haveFocus = 0;
                    if (isSelect) setDirtyRange(selectBegin.vRow, selectEnd.vRow);
                    else setDirtyRange(insertion.vRow, insertion.vRow);
                }
            }
            return 1;            

        case SDL_SPECIAL:
            if ((event->user.code & SDL_IDLE) && (haveFocus)) {
                Uint32 ticks = SDL_GetTicks();
                // (catch wraparound)
                if (ticks < insertBlinkMs) insertBlinkMs = SDL_GetTicks();
                else if (ticks - insertBlinkMs > DELAY_CURSOR_BLINK) {
                    insertBlink = !insertBlink;
                    insertBlinkMs = SDL_GetTicks();
                    setDirtyRange(insertion.vRow, insertion.vRow);
                }
                return 1;
            }
            break;
            
        case SDL_COMMAND:
            switch (event->user.code) {
                case EDIT_COPY:
                    copyText(clipStorage);
                    if (clipStorage.size()) clipboardCopy(clipStorage);
                    return 1;

                case EDIT_CUT:
                    copyText(clipStorage);
                    if (clipStorage.size()) {
                        if (!readonly) pasteText(blankString);
                        clipboardCopy(clipStorage);
                    }
                    return 1;

                case EDIT_PASTE:
                    if ((!readonly) && (canConvertClipboard(CLIPBOARD_TEXT_MULTI))) {
                        clipboardPasteTextMulti(clipStorage);
                        pasteText(clipStorage);
                    }
                    return 1;

                case EDIT_SELECTALL:
                    selectAll();
                    return 1;
            }
            break;

        case SDL_KEYDOWN: {
            int drag = 0;
            Sint32 key;

            if (event->key.keysym.mod & KMOD_SHIFT) {
                drag = 1; // We'll check this if we care about SHIFT
                key = combineKey(event->key.keysym.sym, event->key.keysym.mod & ~KMOD_SHIFT);
            }
            else {
                key = combineKey(event->key.keysym.sym, event->key.keysym.mod);
            }
        
            switch (key) {
                case SDLK_DELETE:
                    if ((drag) || (readonly)) break;
                    keyDelete();
                    return 1;

                case SDLK_BACKSPACE:
                    if ((drag) || (readonly)) break;
                    keyBackspace();
                    return 1;
                    
                case combineKey(SDLK_DELETE, KMOD_CTRL):
                    if ((drag) || (readonly)) break;

                    // If no selection, delete to end of line
                    if (!isSelect) {
                        point = insertion;
                        pointVLine = findVLine(point.vRow);
                        point.column = (*pointVLine).length;
                        insertionState(point, 1);
                    }
                    
                    // Delete current selection
                    keyDelete();
                    return 1;
                    
                case SDLK_RIGHT:
                    point = insertion;
                    goRight(point);
                    insertionState(point, drag);
                    return 1;

                case SDLK_DOWN:
                    point = insertion;
                    ++point.vRow;
                    if (point.vRow < vNumLines) {
                        pointVLine = findVLine(point.vRow);
                        if (point.column > (*pointVLine).length) point.column = (*pointVLine).length;
                        // @TODO: currently goes to same column on next row- NOT
                        // same pixel position; also, remember "virtual" column
                        // to use for all up/down movement
                        insertionState(point, drag);
                    }
                    return 1;

                case SDLK_LEFT:
                    point = insertion;
                    goLeft(point);
                    insertionState(point, drag);
                    return 1;

                case SDLK_UP:
                    point = insertion;
                    --point.vRow;
                    if (point.vRow >= 0) {
                        pointVLine = findVLine(point.vRow);
                        if (point.column > (*pointVLine).length) point.column = (*pointVLine).length;
                        // @TODO: currently goes to same column on previous row- NOT
                        // same pixel position; also, remember "virtual" column
                        // to use for all up/down movement
                        insertionState(point, drag);
                    }
                    return 1;

                case SDLK_END:
                    point = insertion;
                    pointVLine = findVLine(point.vRow);
                    point.column = (*pointVLine).length;
                    insertionState(point, drag);
                    return 1;

                case SDLK_HOME:
                    point = insertion;
                    point.column = 0;
                    insertionState(point, drag);
                    return 1;

                case combineKey(SDLK_UP, KMOD_CTRL):
                    if (myFrame) {
                        myFrame->scrollBy(0, lineHeight);
                    }
                    if (myScroll) {
                        myScroll->scrollBy(0, lineHeight);
                    }
                    return 1;

                case combineKey(SDLK_DOWN, KMOD_CTRL):
                    if (myFrame) {
                        myFrame->scrollBy(0, -lineHeight);
                    }
                    if (myScroll) {
                        myScroll->scrollBy(0, -lineHeight);
                    }
                    return 1;

                case SDLK_PAGEUP:
                    point = insertion;
                    if (point.vRow > 0) {
                        point.vRow -= max(1, (viewHeight / lineHeight) - 1);
                        pointVLine = findVLine(point.vRow);
                        if (point.column > (*pointVLine).length) point.column = (*pointVLine).length;
                        // @TODO: currently goes to same column on target row- NOT
                        // same pixel position; also, remember "virtual" column
                        // to use for all up/down movement
                        insertionState(point, drag);
                    }
                    return 1;

                case SDLK_PAGEDOWN:
                    point = insertion;
                    if (point.vRow < vNumLines - 1) {
                        point.vRow += max(1, (viewHeight / lineHeight) - 1);
                        pointVLine = findVLine(point.vRow);
                        if (point.column > (*pointVLine).length) point.column = (*pointVLine).length;
                        // @TODO: currently goes to same column on target row- NOT
                        // same pixel position; also, remember "virtual" column
                        // to use for all up/down movement
                        insertionState(point, drag);
                    }
                    return 1;

                case combineKey(SDLK_PAGEUP, KMOD_CTRL):
                    if (myFrame) {
                        myFrame->scrollBy(0, max(1, (viewHeight / lineHeight) - 1) * lineHeight);
                    }
                    if (myScroll) {
                        myScroll->scrollBy(0, max(1, (viewHeight / lineHeight) - 1) * lineHeight);
                    }
                    return 1;

                case combineKey(SDLK_PAGEDOWN, KMOD_CTRL):
                    if (myFrame) {
                        myFrame->scrollBy(0, max(1, (viewHeight / lineHeight) - 1) * -lineHeight);
                    }
                    if (myScroll) {
                        myScroll->scrollBy(0, max(1, (viewHeight / lineHeight) - 1) * -lineHeight);
                    }
                    return 1;

                case combineKey(SDLK_HOME, KMOD_CTRL):
                    point.vRow = 0;
                    point.column = 0;
                    insertionState(point, drag);
                    return 1;

                case combineKey(SDLK_END, KMOD_CTRL):
                    point.vRow = vNumLines - 1;
                    pointVLine = findVLine(point.vRow);
                    point.column = (*pointVLine).length;
                    insertionState(point, drag);
                    return 1;

                case combineKey(SDLK_RIGHT, KMOD_CTRL):
                    // Current position
                    point = insertion;
                    pointVLine = findVLine(point.vRow);
                    
                    // Last character? Next line
                    if (point.column == (*pointVLine).length) {
                        // Last character of last line?
                        if (point.vRow == vNumLines - 1) return 1;
                        goRight(point);
                        pointVLine = findVLine(point.vRow);
                        foundStr = 0;
                    }
                    else {
                        // Next not word character
                        foundStr = (*pointVLine).getStr().find_first_not_of(wordChars, point.column);
                        if (foundStr == string::npos) foundStr = (*pointVLine).length;
                    }
                    
                    // Next word character
                    foundStr = (*pointVLine).getStr().find_first_of(wordChars, foundStr);
                    if (foundStr == string::npos) foundStr = (*pointVLine).length;
                    
                    point.column = foundStr;
                    insertionState(point, drag);
                    return 1;
                    
                case combineKey(SDLK_LEFT, KMOD_CTRL):
                    // Current position
                    point = insertion;
                    pointVLine = findVLine(point.vRow);
                    
                    if (point.column == 0) {
                        point.column = 0;
                        goLeft(point);
                    }
                    else {
                        // Previous word character then not word character
                        foundStr = (*pointVLine).getStr().find_last_of(wordChars, point.column - 1);
                        if (foundStr == string::npos) {
                            // Last character, previous line
                            point.column = 0;
                            goLeft(point);
                        }
                        else {
                            foundStr = (*pointVLine).getStr().find_last_not_of(wordChars, foundStr);
                            if (foundStr == string::npos) foundStr = 0;
                            else if (foundStr > 0) ++foundStr;
                            point.column = foundStr;
                        }
                    }
                    
                    insertionState(point, drag);
                    return 1;

                case SDLK_TAB:
                    if (readonly) break;
                
                    if (isSelect) {
                        // indent/unindent block if a selection
                        int dStart = (*findVLine(selectBegin.vRow)).dLine;
                        int dEnd = (*findVLine(selectEnd.vRow)).dLine;
                        if (drag)
                            unindent(dStart, dEnd - dStart + 1);
                        else
                            indent(dStart, dEnd - dStart + 1);
                    }
                    else {
                        if (drag) {
                            // Go left to first multiple of TAB_SIZE as long as only spaces in our way
                            string line = (*findVLine(insertion.vRow)).getStr();
                            if (line[insertion.column - 1] == ' ') keyBackspace();
                            while ((insertion.column % TAB_SIZE) && (line[insertion.column - 1] == ' ')) {
                                keyBackspace();
                            }
                        }
                        else {
                            // Insert spaces until multiple of TAB_SIZE
                            pasteText(" ");
                            while (insertion.column % TAB_SIZE) {
                                pasteText(" ");
                            }
                        }
                    }
                    
                    return 1;
                    
                case SDLK_RETURN:
                case SDLK_KP_ENTER: {
                    if (readonly) break;

                    // (get line now, for auto-indent later)
                    string line = *(*findVLine(insertion.vRow)).dLineI;

                    string toInsert("\n");
                    pasteText(toInsert);
                    
                    // Auto-indent to match previous line
                    string::size_type pos = line.find_first_not_of(' ', 0);
                    if (pos == string::npos) pos = line.size();
                    
                    // Adjust indent by counting ([{ vs }])
                    if (autoIndent) {
                        int adjust = 0;
                        for (int c = line.size() - 1; c >= 0; --c) {\
                            if ((line[c] == '(') || (line[c] == '[') || (line[c] == '{'))
                                ++adjust;
                            else if ((line[c] == ')') || (line[c] == ']') || (line[c] == '}'))
                                --adjust;
                        }

                        if (adjust > 0)
                            pos += TAB_SIZE;
                    }

                    if (pos > 0) {
                        string spaces(pos, ' ');
                        pasteText(spaces);
                    }
                    
                    return 1;
                }
                    
                default:
                    if (readonly) break;

                    if ((event->key.keysym.unicode) && !(event->key.keysym.mod & KMOD_ALT)) {
                        if (event->key.keysym.unicode & 0xFF80) {
                            key = '?';
                        }
                        else {
                            key = event->key.keysym.unicode;
                            if (key < 32) break;
                        }
                        string toInsert(1, key);
                        pasteText(toInsert);
                        
                        if ((autoIndent) && ((key == '}') || (key == ']'))) {
                            // Previous line must exist
                            list<VLine>::iterator curV = findVLine(insertion.vRow);
                            if ((*curV).dLine > 0) {
                                // Indent on current
                                list<string>::iterator dl = (*curV).dLineI;
                                string line = *dl;
                                string::size_type pos = line.find_first_not_of(' ', 0);
                                
                                // Indent on previous must not be greater
                                // @TODO: also, if same indent and previous had the extra {, don't unindent
                                --dl;
                                line = *dl;
                                if (line.find_first_not_of(' ', 0) <= pos) {
                                    // Unindent curent line
                                    unindent((*curV).dLine, 1);
                                }
                            }
                        }
                        
                        return 1;
                    }
                    break;
            }
            break;
        }
    }
    
    return 0;
}

void EditBox::load() { start_func
    if (open) {
        // Force reinit
        wrappedTo = -1;
        prepOpen();
    }
}

void EditBox::apply() { start_func
    // (nothing)
}

list<EditBox::VLine>::iterator EditBox::findDLine(int dLine, int* getVLineNum) { start_func
    // Out of range?
    if ((dLine < 0) || (dLine >= dNumLines)) {
        if (getVLineNum) *getVLineNum = -1;
        return lineMap.end();
    }
    
    // Special case
    if (dLine == 0) {
        if (getVLineNum) *getVLineNum = 0;
        return lineMap.begin();
    }

    // Starts at beginning, end, or lastSpot, whichever is closest
    // Beginning
    int startLine = 0;
    int vLine = 0;
    list<VLine>::iterator startIter = lineMap.begin();
    
    // Is end closer?
    if (dNumLines - dLine < dLine) {
        vLine = vNumLines;
        startLine = dNumLines;
        startIter = lineMap.end();
    }
    
    // Is lastSpot closer?
    if (lastSpotLine >= 0) {
        if (abs((*lastSpot).dLine - dLine) < abs(startLine - dLine)) {
            vLine = lastSpotLine;
            startLine = (*lastSpot).dLine;
            startIter = lastSpot;
        }
    }
    
    // Iterate! Backwards first, stop at PREVIOUS dline
    // (safe because we special-cased line 0 previously)
    while (startLine >= dLine) {
        --startIter;
        --vLine;
        startLine = (*startIter).dLine;
    }

    // Forwards? (at least once)
    // This way we know we're at the first instance of a given dline
    while (startLine < dLine) {
        ++startIter;
        ++vLine;
        startLine = (*startIter).dLine;
    }

    if (getVLineNum) *getVLineNum = vLine;
    
    return startIter;
}

list<EditBox::VLine>::iterator EditBox::findVLine(int line) { start_func
    // Out of range?
    if ((line < 0) || (line >= vNumLines)) return lineMap.end();

    // Starts at beginning, end, or lastSpot, whichever is closest
    // Beginning
    int startLine = 0;
    list<VLine>::iterator startIter = lineMap.begin();
    
    // Is end closer?
    if (vNumLines - line < line) {
        startLine = vNumLines;
        startIter = lineMap.end();
    }
    
    // Is lastSpot closer?
    if (lastSpotLine >= 0) {
        if (abs(lastSpotLine - line) < abs(startLine - line)) {
            startLine = lastSpotLine;
            startIter = lastSpot;
        }
    }
    
    // Iterate! Forwards?
    while (startLine < line) {
        ++startLine;
        ++startIter;
    }

    // Backwards?
    while (startLine > line) {
        --startLine;
        --startIter;
    }
    
    // Retain for next time
    if (startLine != lastSpotLine) {
        lastSpotLine = startLine;
        lastSpot = startIter;
    }
    
    return startIter;
}

void EditBox::paintText(const string& text, const string& entireLine, int posInLine, int x, int y, SDL_Surface* destSurface, int isSelected) const { start_func
    drawText(text, guiRGB[isSelected ? COLOR_TEXTBOX : COLOR_TEXT], x, y, destSurface, font);
}

void EditBox::display(SDL_Surface* destSurface, Rect& toDisplay, const Rect& clipArea, int xOffset, int yOffset) { start_func
    assert(destSurface);

    if (visible) {
        // If dirty, redraw range or all
        if (dirty) {
            // Always redraw entire width
            Rect toDraw;
            getRect(toDraw);
            toDraw.x += xOffset;
            toDraw.y += yOffset;
            // Total dirty- as is
            if (!totalDirty) {
                // Not total dirty- do only dirty lines
                toDraw.y += vDirtyFirst * lineHeight + GUI_EDITBOX_INNERMARGIN;
                toDraw.h = (vDirtyLast - vDirtyFirst + 1) * lineHeight;
            }
            dirty = totalDirty = 0;
            vDirtyFirst = vDirtyLast = -1;
            
            // Add our redraw into display rect and clip
            boundRects(toDisplay, toDraw);
            intersectRects(toDisplay, clipArea);
        }
        
        // Anything to draw?
        if (toDisplay.w) {
            SDL_SetClipRect(destSurface, &toDisplay);
        
            xOffset += x;
            yOffset += y;

            // This widget attempts to only redraw lines within the
            // dirtied area.

            // Background
            SDL_FillRect(destSurface, &toDisplay, guiPacked[COLOR_TEXTBOX]);
            
            // Draw all lines we can see
            int pos = (toDisplay.y - yOffset - GUI_EDITBOX_INNERMARGIN) / lineHeight;
            if (pos < 0) pos = 0;
            if (pos >= vNumLines) pos = vNumLines - 1;

            // (add in height - 1 so it rounds up)
            int last = (toDisplay.y + toDisplay.h - yOffset - GUI_EDITBOX_INNERMARGIN + lineHeight - 1) / lineHeight;
            if (last < 0) last = 0;
            if (last >= vNumLines) last = vNumLines - 1;
            
            // Need an iterator pointing at first line
            list<VLine>::iterator spot = findVLine(pos);
            
            for (; pos <= last; ++pos, ++spot) {
                VLine& vl = *spot;
                int drewText = 0;
                
                if (haveFocus) {
                    if ((isSelect) && (pos >= selectBegin.vRow) && (pos <= selectEnd.vRow)) {
                        int beginX = pos == selectBegin.vRow ? selectBegin.pixelX : 0;
                        int endX = pos == selectEnd.vRow ? selectEnd.pixelX : vLongestLength;
                        // (can have an "invisible" selection at end or beginning of a row)
                        if (endX > beginX) {
                            int beginCol = pos == selectBegin.vRow ? selectBegin.column : 0;
                            int endCol = pos == selectEnd.vRow ? selectEnd.column : vl.length;
                            
                            if ((beginCol > 0) || (endCol < vl.length)) {
                                // Paint all text, to show unselected area
                                paintText(vl.getStr(), *vl.dLineI, vl.colStart,
                                          xOffset + GUI_EDITBOX_INNERMARGIN + GUI_EDITBOX_LEFTPAD,
                                          lineHeight * pos + yOffset + GUI_EDITBOX_INNERMARGIN, destSurface);
                            }
                                 
                            drawGradient(xOffset + GUI_EDITBOX_INNERMARGIN + GUI_EDITBOX_LEFTPAD + beginX,
                                         lineHeight * pos + yOffset + GUI_EDITBOX_INNERMARGIN,
                                         endX - beginX, lineHeight,
                                         guiRGB[COLOR_SELECTION1], guiRGB[COLOR_SELECTION2], destSurface);
                                     
                            // Overwrite selected text    
                            paintText((*vl.dLineI).substr(vl.colStart + beginCol, endCol - beginCol),
                                      *vl.dLineI, vl.colStart + beginCol,
                                      xOffset + GUI_EDITBOX_INNERMARGIN + GUI_EDITBOX_LEFTPAD + beginX,
                                      lineHeight * pos + yOffset + GUI_EDITBOX_INNERMARGIN, destSurface, 1);
                                         
                            drewText = 1;
                        }
                    }
                }
                
                // Paint text if didn't do during selection
                if (!drewText) {
                    paintText(vl.getStr(), *vl.dLineI, vl.colStart,
                              xOffset + GUI_EDITBOX_INNERMARGIN + GUI_EDITBOX_LEFTPAD,
                              lineHeight * pos + yOffset + GUI_EDITBOX_INNERMARGIN, destSurface);
                }
            
                if ((haveFocus) && (insertBlink) && (insertion.vRow == pos)) {
                    // Insertion point
                    drawRect(xOffset + GUI_EDITBOX_INNERMARGIN + GUI_EDITBOX_LEFTPAD + insertion.pixelX - 1,
                             lineHeight * pos + yOffset + GUI_EDITBOX_INNERMARGIN + insertYOffset,
                             GUI_EDITBOX_INSERTPOINTWIDTH, insertHeight,
                             guiPacked[COLOR_CURSOR], destSurface);
                }
            }
        }
    }
}

Window::CommandSupport EditBox::supportsCommand(int code) const { start_func
    switch (code) {
        case EDIT_COPY:
        case EDIT_CUT:
            if (isSelect) return Window::COMMAND_ENABLE;
            return Window::COMMAND_DISABLE;
        
        case EDIT_PASTE:
            if (canConvertClipboard(CLIPBOARD_TEXT_MULTI)) return Window::COMMAND_ENABLE;
            return Window::COMMAND_DISABLE;

        case EDIT_SELECTALL:
            return Window::COMMAND_ENABLE;
    }
    
    return Window::COMMAND_HIDE;
}

void EditBox::addTo(Dialog* dialog, int showWidth, int showLines) { start_func
    assert(!myFrame);
    assert(!myScroll);
    assert(dialog);

    // With this set, when we get a resize() (from our parents, below) we'll
    // initialize everything automatically
    wrappedTo = -1;

    // Set to standard size
    if (showWidth <= 0) {
        string standard(GUI_EDITBOX_DEFAULTWIDTH, 'X');
        showWidth = fontWidth(standard, font);
    }

    if (showLines <= 0) {
        showLines = GUI_EDITBOX_DEFAULTLINES;
    }

    myScroll = new WidgetScroll(WidgetScroll::FRAMETYPE_BEVEL, this, showWidth, showLines * lineHeight);
    dialog->addWidget(myScroll);
    
    // Just in case parents didn't resize us, this triggers initialization
    prepOpen();
}

EditBox::TextPoint EditBox::insertWhere(int x, int y) { start_func
    // Determine row
    int row = (y - GUI_EDITBOX_INNERMARGIN) / lineHeight;
    if (row < 0) row = 0;
    if (row >= vNumLines) row = vNumLines - 1;
    return insertWhereRow(x, row);
}

EditBox::TextPoint EditBox::insertWhereRow(int x, int row) { start_func
    TextPoint result;
    result.vRow = row;

    // Adjust by insertion point width for a bit of a fudge
    x -= GUI_EDITBOX_INNERMARGIN + GUI_EDITBOX_LEFTPAD - GUI_EDITBOX_INSERTPOINTWIDTH;
    result.column = breakLineBefore((*findVLine(row)).getStr(), x);
    return result;
}

int EditBox::breakLineBefore(const string& line, int xPixel) { start_func
    // (special case of 0 pixel would automatically drop to 0 during algorithm,
    // but less-than-0 needs to be covered anyways)
    if (xPixel <= 0) return 0;

    // Start at end of line (special case)
    int widthPixels = fontWidth(line, font);
    int widthChars = line.size();
    if (widthPixels <= xPixel) return widthChars;
    
    // 1 = too long, -1 = too short
    int lastResult = 1;
    
    while (1) {
        // Amount we're off in pixels (negative = move left)
        int changePixels = xPixel - widthPixels;
        if (!changePixels) changePixels = 1;
        // Estimated change in characters
        int changeChars = changePixels * widthChars / widthPixels;
        // Change must be non-zero
        if (!changeChars) changeChars = changePixels > 0 ? 1 : -1;
        widthChars += changeChars;
        // Result (1 too long, -1 too short)
        widthPixels = fontWidth(line.substr(0, widthChars), font);
        int thisResult = widthPixels <= xPixel ? -1 : 1;
        // We're done when the result is "too short", previous result was "too long",
        // and we just moved a single character back
        if ((thisResult < lastResult) && (changeChars == -1)) return widthChars;
        lastResult = thisResult;
    }
}
    
void EditBox::keyBackspace() { start_func
    if (!isSelect) {
        TextPoint point = insertion;
        
        // If only spaces to our left and non-space to our right, unindent
        string line = (*findVLine(point.vRow)).getStr();
        string::size_type pos = line.find_first_not_of(' ', 0);
        if ((point.column > 0) && (((int)pos == point.column) || ((pos == string::npos) && (point.column == (int)line.size())))) {
            // Go left to first multiple of TAB_SIZE
            goLeft(point);
            while (point.column % TAB_SIZE) {
                goLeft(point);
            }
        }
        else {
            // Go left one character
            goLeft(point);
        }
        insertionState(point, 1);
    }
    pasteText(blankString);
}

void EditBox::keyDelete() { start_func
    if (!isSelect) {
        TextPoint point = insertion;
        
        // If right-most character, delete all spaces at beginning of next line as well
        string line = (*findVLine(point.vRow)).getStr();
        if (point.column == (int)line.size()) {
            // Go right until a non-space or end of next line
            goRight(point);
            string line = (*findVLine(point.vRow)).getStr();
            int max = line.size();
            while ((point.column < max) && (line[point.column] == ' ')) {
                goRight(point);
            }
        }
        else {
            // Go right one character
            goRight(point);
        }
        insertionState(point, 1);
    }
    pasteText(blankString);
}

void EditBox::modifyDLine(int dLine) { start_func
    int erased, added, modLine;
    
    eraseLineMap(dLine, 1, &modLine, &erased);
    insertLineMap(dLine, 1, NULL, &added);

    int change = erased - added;
    if ((change) && (insertion.vRow >= dLine)) {
        // Scroll to ensure same view
        if (myFrame) {
            myFrame->scrollBy(0, change * lineHeight);
        }
        if (myScroll) {
            myScroll->scrollBy(0, change * lineHeight);
        }
    }

    // Resize
    resize(GUI_EDITBOX_INNERMARGIN * 2 + GUI_EDITBOX_LEFTPAD + GUI_EDITBOX_RIGHTPAD + vLongestLength,
           GUI_EDITBOX_INNERMARGIN * 2 + vNumLines * lineHeight);
           
    if (added == erased)
        setDirtyRange(modLine, modLine + added - 1);
    else
        setDirtyRange(modLine, vNumLines - 1);
}

void EditBox::doIndent(int dLine, int count, int function) { start_func
    // Clip range
    if (dLine < 0) {
        count += dLine;
        dLine = 0;
    }
    if (dLine + count > dNumLines)
        count = dNumLines - dLine;
    if (count <= 0)
        return;
    
    contentModifiedPre(EDITBOX_MODIFY_LINE, dLine, count);
    contentModifiedPre(EDITBOX_MODIFY_DONE, 0, 0);
    vLineToDLineAllPoints();
    
    list<VLine>::iterator vl = findDLine(dLine);
    assert(vl != lineMap.end());
    list<string>::iterator dl = (*vl).dLineI;
    int dlNum = dLine;
            debugStdout("start");
    do {
        assert(dl != contents->end());
        string& mod = *dl;
            
        int trim;
        if (function) {
            // Indent
            mod = string(TAB_SIZE, ' ') + mod;
            trim = TAB_SIZE;
        }
        else {
            // Unindent
            trim = mod.find_first_not_of(' ', 0);
            if (trim == (int)string::npos)
                trim = mod.size();
            trim = min((int)TAB_SIZE, trim);
            mod = mod.substr(trim, string::npos);
            trim = -trim;
        }
        
        modifyDLine(dlNum);
        
        // (affects dline position before converting back to vlines)
        if (insertion.vRow == dlNum) insertion.column = max(0, insertion.column + trim);
        if (selectBegin.vRow == dlNum) selectBegin.column = max(0, selectBegin.column + trim);
        if (selectEnd.vRow == dlNum) selectEnd.column = max(0, selectEnd.column + trim);
        
        ++dl;
        ++dlNum;
    } while (--count);
    
    dLineToVLineAllPoints();
    contentModifiedPost(EDITBOX_MODIFY_LINE, dLine, count);
    contentModifiedPost(EDITBOX_MODIFY_DONE, 0, 0);
}

void EditBox::indent(int dLine, int count) { start_func
    doIndent(dLine, count, 1);
}

void EditBox::unindent(int dLine, int count) { start_func
    doIndent(dLine, count, 0);
}

void EditBox::getCursorData(TextPoint& ins, TextPoint& selBegin, TextPoint& selEnd) const { start_func
    ins = insertion;
    if (isSelect) {
        selBegin = selectBegin;
        selEnd = selectEnd;
    }
    else {
        selBegin = selEnd = ins;
    }
}

void EditBox::setCursorData(TextPoint& ins, TextPoint& selBegin, TextPoint& selEnd) { start_func
    if ((selBegin.vRow != selEnd.vRow) || (selBegin.column != selEnd.column)) {
        if ((selBegin.vRow == ins.vRow) && (selBegin.column == ins.column)) {
            insertionState(selEnd, 0);
            insertionState(selBegin, 1);
        }
        else if ((selEnd.vRow == ins.vRow) && (selEnd.column == ins.column)) {
            insertionState(selBegin, 0);
            insertionState(selEnd, 1);
        }
        else {
            insertionState(ins, 0);
        }
    }
    else {
        insertionState(ins, 0);
    }
}
    
const EditBox::TextPoint& EditBox::insertionState() const { start_func
    return insertion;
}

const EditBox::TextPoint& EditBox::insertionState(TextPoint& newPos, int drag) { start_func
    // Clip
    if (newPos.vRow < 0) newPos.vRow = 0;
    if (newPos.vRow >= vNumLines) newPos.vRow = vNumLines - 1;
    VLine& vl = *findVLine(newPos.vRow);
    if (newPos.column < 0) newPos.column = 0;
    if (newPos.column > vl.length) newPos.column = vl.length;

    // First, find correct pixelX
    newPos.pixelX = fontWidth((*vl.dLineI).substr(vl.colStart, newPos.column), font);
    
    // Drag?
    if (drag) {
        int vDirtyFirst;
        int vDirtyLast;
    
        // No current selection?
        if (!isSelect) {
            selectBegin = selectEnd = insertion;
        }
        
        // If selection beginning matches insert point
        if ((selectBegin.vRow == insertion.vRow) && (selectBegin.column == insertion.column)) {
            // Drag beginning point
            vDirtyFirst = selectBegin.vRow;
            vDirtyLast = newPos.vRow;
            selectBegin = insertion = newPos;
        }
        else {
            // Drag end point
            vDirtyFirst = selectEnd.vRow;
            vDirtyLast = newPos.vRow;
            selectEnd = insertion = newPos;
        }
        
        // No selection?
        if ((selectBegin.vRow == selectEnd.vRow) && (selectBegin.column == selectEnd.column)) {
            isSelect = 0;
        }
        else {
            isSelect = 1;
        
            // Ensure in proper order
            if ((selectEnd.vRow < selectBegin.vRow) ||
                ((selectEnd.vRow == selectBegin.vRow) &&
                 (selectEnd.column < selectBegin.column))) {
                swap(selectBegin, selectEnd);
            }
        }
        
        // Dirty
        if (vDirtyFirst > vDirtyLast) swap(vDirtyFirst, vDirtyLast);
        setDirtyRange(vDirtyFirst, vDirtyLast);
    }
    else {
        // Dirty previous
        if (isSelect) setDirtyRange(selectBegin.vRow, selectEnd.vRow);
        else setDirtyRange(insertion.vRow, insertion.vRow);

        // Reset selection
        isSelect = 0;

        // Insertion point
        insertion = newPos;
    }
    
    // Scroll
    int scrollLeft = insertion.pixelX + GUI_EDITBOX_INNERMARGIN + GUI_EDITBOX_LEFTPAD - 1 - GUI_EDITBOX_SCROLLAHEAD;
    if (scrollLeft < 0) scrollLeft = 0;
    if (myFrame) {
        myFrame->scrollToView(scrollLeft,
                              insertion.vRow * lineHeight + GUI_EDITBOX_INNERMARGIN,
                              GUI_EDITBOX_INSERTPOINTWIDTH + GUI_EDITBOX_SCROLLAHEAD * 2, lineHeight);
    }
    if (myScroll) {
        myScroll->scrollToView(scrollLeft,
                               insertion.vRow * lineHeight + GUI_EDITBOX_INNERMARGIN,
                               GUI_EDITBOX_INSERTPOINTWIDTH + GUI_EDITBOX_SCROLLAHEAD * 2, lineHeight);
    }

    // (dirties current row)
    setDirtyAndBlink();
    return insertion;
}

void EditBox::selectAll() { start_func
    // First point
    TextPoint point;
    point.vRow = 0;
    point.column = 0;
    insertionState(point, 0);
    
    // Last point
    point.vRow = vNumLines - 1;
    point.column = (*findVLine(point.vRow)).length;
    insertionState(point, 1);
}

void EditBox::contentModifyHelper(int vLine, int vCount) { start_func
    // vcount can be positive (inserted lines) or negative (removed lines)
    // in addition, any line can have been modified 'in place'
    TextPoint newInsertion = insertion;
    TextPoint newBegin = selectBegin;
    TextPoint newEnd = selectEnd;
    
    // Insertion
    if (vCount > 0) {
        if (newInsertion.vRow >= vLine) {
            newInsertion.vRow += vCount;
            // Scroll to ensure same view
            if (myFrame) {
                myFrame->scrollBy(0, -vCount * lineHeight);
            }
            if (myScroll) {
                myScroll->scrollBy(0, -vCount * lineHeight);
            }
        }
        if (isSelect) {
            if (newBegin.vRow >= vLine) {
                newBegin.vRow += vCount;
            }
            if (newEnd.vRow >= vLine) {
                newEnd.vRow += vCount;
            }
        }
    }
    
    // Deletion
    else if (vCount < 0) {
        if (newInsertion.vRow >= vLine) {
            if (newInsertion.vRow >= vLine - vCount) {
                // past affected area
                newInsertion.vRow += vCount;
                // Scroll to ensure same view
                if (myFrame) {
                    myFrame->scrollBy(0, -vCount * lineHeight);
                }
                if (myScroll) {
                    myScroll->scrollBy(0, -vCount * lineHeight);
                }
            }
            else {
                // within affected area
                newInsertion.vRow = vLine;
                newInsertion.column = 0;
                if (newInsertion.vRow >= vNumLines) {
                    newInsertion.vRow = vNumLines - 1;
                    newInsertion.column = 99999;
                }
            }
        }
        if (isSelect) {
            if (newBegin.vRow >= vLine) {
                if (newBegin.vRow >= vLine - vCount) {
                    // past affected area
                    newBegin.vRow += vCount;
                }
                else {
                    // within affected area
                    newBegin.vRow = vLine;
                    newBegin.column = 0;
                    if (newBegin.vRow >= vNumLines) {
                        newBegin.vRow = vNumLines - 1;
                        newBegin.column = MAX_LINELENGTH + 1;
                    }
                }
            }
            if (newEnd.vRow >= vLine) {
                if (newEnd.vRow >= vLine - vCount) {
                    // past affected area
                    newEnd.vRow += vCount;
                }
                else {
                    // within affected area
                    newEnd.vRow = vLine;
                    newEnd.column = 0;
                    if (newEnd.vRow >= vNumLines) {
                        newEnd.vRow = vNumLines - 1;
                        newEnd.column = MAX_LINELENGTH + 1;
                    }
                }
            }
        }
    }
    
    // Verify columns
    int len = (*findVLine(newInsertion.vRow)).length;
    if (len < newInsertion.column) newInsertion.column = len;
    if (isSelect) {
        len = (*findVLine(newBegin.vRow)).length;
        if (len < newBegin.column) newBegin.column = len;
        len = (*findVLine(newEnd.vRow)).length;
        if (len < newEnd.column) newEnd.column = len;
    }
    
    // Resize
    resize(GUI_EDITBOX_INNERMARGIN * 2 + GUI_EDITBOX_LEFTPAD + GUI_EDITBOX_RIGHTPAD + vLongestLength,
           GUI_EDITBOX_INNERMARGIN * 2 + vNumLines * lineHeight);
    
    // Set new positions
    if (isSelect) {
        if ((newBegin.column == newInsertion.column) && (newBegin.vRow == newInsertion.vRow))
            insertionState(newEnd, 0);
        else
            insertionState(newBegin, 0);
        insertionState(newInsertion, 1);
    }
    else
        insertionState(newInsertion, 0);
}

void EditBox::contentModify(int dLine, int countLines) { start_func
    int modLine, erased, added;

    eraseLineMap(dLine, countLines, &modLine, &erased);
    insertLineMap(dLine, countLines, &modLine, &added);
    contentModifyHelper(modLine + min(added, erased), added - erased);
    if (added == erased)
        setDirtyRange(modLine, modLine + added - 1);
    else
        setDirtyRange(modLine, vNumLines - 1);
}

void EditBox::contentInsert(int dLine, int countLines) { start_func
    int modLine, added;

    insertLineMap(dLine, countLines, &modLine, &added);
    contentModifyHelper(modLine, added);
    setDirtyRange(modLine, vNumLines - 1);
}

void EditBox::contentDelete(int dLine, int countLines) { start_func
    int modLine, erased;

    eraseLineMap(dLine, countLines, &modLine, &erased);
    contentModifyHelper(modLine, -erased);
    setDirtyRange(modLine, vNumLines - 1);
}

void EditBox::pasteText(const string& text) { start_func
    // Most of our work is done on dlines, not vlines
    VLine* insertionVL = &*findVLine(insertion.vRow);
    VLine* beginVL = &*findVLine(isSelect ? selectBegin.vRow : insertion.vRow);
    VLine* endVL = &*findVLine(isSelect ? selectEnd.vRow : insertion.vRow);

    // Count lines being inserted
    string::size_type spos = 0;
    int insertedDLines = 0;
    while ((spos = text.find_first_of('\n', spos)) != string::npos) {
        ++insertedDLines;
        ++spos;
    }

    // Start by alerting to incoming modifications
    int r1 = -1, r2 = 0, i1 = -1, m1;
    if (beginVL->dLine != endVL->dLine) contentModifiedPre(EDITBOX_REMOVE_LINES, r1 = beginVL->dLine + 1, r2 = endVL->dLine - beginVL->dLine);
    if (insertedDLines) contentModifiedPre(EDITBOX_INSERT_LINES, i1 = beginVL->dLine + 1, insertedDLines);
    contentModifiedPre(EDITBOX_MODIFY_LINE, m1 = beginVL->dLine, 1);
    contentModifiedPre(EDITBOX_MODIFY_DONE, 0, 0);

    // First, clear selection, if any
    if (isSelect) {
        list<string>::iterator pos = beginVL->dLineI;
        list<string>::iterator end = endVL->dLineI;

        // One row?
        if (pos == end) {
            (*pos).erase(beginVL->colStart + selectBegin.column, endVL->colStart + selectEnd.column - beginVL->colStart - selectBegin.column);
        }
        else {
            list<string>::iterator first = pos;

            // To end of first line
            (*pos).erase(beginVL->colStart + selectBegin.column, (*pos).size());

            // Lines in between
            ++pos;
            list<string>::iterator next = pos;
            for (; pos != end; pos = next) {
                ++next;
                contents->erase(pos);
            }

            // Last line- start to middle
            (*pos).erase(0, endVL->colStart + selectEnd.column);

            // Combine first and last lines
            // Combine
            *first += *pos;
            contents->erase(pos);
        }

        // selectBegin is still entirely valid
        insertion = selectBegin;
        insertionVL = beginVL;
        isSelect = 0;
    }

    // Insert
    spos = 0;
    string::size_type prev = spos;
    int insertAt = insertionVL->colStart + insertion.column;
    list<string>::iterator insertStr = insertionVL->dLineI;

    // Split at every \n
    int inserted = 0;
    while ((spos = text.find_first_of('\n', spos)) != string::npos) {
        // Insert new line containing text after insertion point
        list<string>::iterator nextStr = insertStr;
        ++nextStr;
        contents->insert(nextStr, (*insertStr).substr(insertAt, string::npos));

        // Delete text we moved forward
        (*insertStr).erase(insertAt, string::npos);
        
        // Insert text
        (*insertStr).insert(insertAt, text.substr(prev, spos - prev));

        // Move insertion point to beginning of next (newly added) line
        insertAt = 0;
        ++insertStr;

        ++spos;
        prev = spos;
        ++inserted;
    }

    // One final portion to insert
    int lenToInsert = text.substr(prev, string::npos).size();
    (*insertStr).insert(insertAt, text.substr(prev, lenToInsert));

    // (will disappear after calling erase)
    int iVLdL = insertionVL->dLine;

    // Handles modifying both numlines and linemap
    int eCount;
    eraseLineMap(beginVL->dLine, endVL->dLine - beginVL->dLine + 1, NULL, &eCount);

    // Handles longest length, modifying both numlines and linemap
    int iStart, iCount;
    insertLineMap(iVLdL, inserted + 1, &iStart, &iCount);

    // Resize
    resize(GUI_EDITBOX_INNERMARGIN * 2 + GUI_EDITBOX_LEFTPAD + GUI_EDITBOX_RIGHTPAD + vLongestLength,
           GUI_EDITBOX_INNERMARGIN * 2 + vNumLines * lineHeight);
    
    // Move insertion point
    TextPoint newinsert = insertion;
    newinsert.column = insertAt + lenToInsert;
    newinsert.vRow = iStart + iCount - 1;
    insertionVL = &*findVLine(newinsert.vRow);
    while (newinsert.column < insertionVL->colStart) {
        insertionVL = &*findVLine(--newinsert.vRow);
    }
    newinsert.column -= insertionVL->colStart;
    insertionState(newinsert);

    // Alert to finished modifications
    if (r1 >= 0) contentModifiedPost(EDITBOX_REMOVE_LINES, r1, r2);
    if (i1 >= 0) contentModifiedPost(EDITBOX_INSERT_LINES, i1, insertedDLines);
    contentModifiedPost(EDITBOX_MODIFY_LINE, m1, 1);
    contentModifiedPost(EDITBOX_MODIFY_DONE, 0, 0);

    // dirty
    if (eCount == iCount)
        setDirtyRange(iStart, iStart + eCount - 1);
    else
        setDirtyRange(iStart, vNumLines - 1);
}

void EditBox::copyText(string& buffer) { start_func
    if (isSelect) {
        VLine* beginVL = &*findVLine(selectBegin.vRow);
        VLine* endVL = &*findVLine(selectEnd.vRow);
        list<string>::iterator pos = beginVL->dLineI;
        list<string>::iterator end = endVL->dLineI;
        
        // One row?
        if (pos == end) {
            buffer = (*pos).substr(beginVL->colStart + selectBegin.column, endVL->colStart + selectEnd.column - beginVL->colStart - selectBegin.column);
        }
        else {
            // To end of first line
            buffer = (*pos).substr(beginVL->colStart + selectBegin.column, string::npos);
            
            // Lines in between
            for (++pos; pos != end; ++pos) {
                buffer += "\n";
                buffer += *pos;
            }
            
            // Last line- start to middle
            buffer += "\n";
            buffer += (*pos).substr(0, endVL->colStart + selectEnd.column);
        }
    }
    else {
        buffer = blankString;
    }
}

Window::WindowType EditBox::windowType() const { start_func
    return WINDOW_CLIENT;
}

void EditBox::contentModifiedPre(ContentChangeType type, int firstRow, int numRows) { start_func
    // (default does nothing)
}

void EditBox::contentModifiedPost(ContentChangeType type, int firstRow, int numRows) { start_func
    // (default does nothing)
}

void EditBox::updateTitlebar() { start_func
    if (myFrame) {
        myFrame->setTitle("(no title)");
    }
}

FrameWindow* EditBox::runWindowed() { start_func
    assert(!myScroll);

    // Prevent duplication
    if (myFrame) {
        desktop->bringToTop(myFrame);
        return myFrame;
    }

    // With this set, when we get a resize() (from our parents, below) we'll
    // initialize everything automatically
    wrappedTo = -1;

    // We remember the frame pointer even though it'll delete itself
    myFrame = new FrameWindow(blankString, FrameWindow::RESIZING_NORMAL, FrameWindow::FRAMETYPE_BEVEL_TEXT, this);
    open = 1;
    
    // Cascade
    myFrame->show(FrameWindow::SHOW_CASCADE, FrameWindow::SHOW_CASCADE, FrameWindow::SHOW_CASCADE, FrameWindow::SHOW_CASCADE);

    // Just in case parents didn't resize us, this triggers initialization
    prepOpen();

    // Scroll to cursor
    insertionState(insertion, isSelect);

    updateTitlebar();
    return myFrame;
}

